﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
using Microsoft.Surface.Presentation.Input;
using ZOIL.Framework.Core;
using ZOIL.Framework.Presentation;

namespace ZOIL.Library.Behaviors
{
    public static class ZObjectSurface2Handler
    {
        private static readonly Dictionary<ZComponent, ZObjectManipulationInformation> ElementsBeingManipulated =
            new Dictionary<ZComponent, ZObjectManipulationInformation>();

        #region Eventhandler

        public static void OnContactTapGesture(object sender, TouchEventArgs e)
        {
            if (!e.TouchDevice.GetIsFingerRecognized())
            {
                return;
            }

            FrameworkElement zoomTarget = ZTreeHelper.GetClosestZoomTarget(sender as FrameworkElement, e.Source as FrameworkElement);
            if (zoomTarget != null)
            {
                var landscape = ZTreeHelper.TryFindParent<ZInformationLandscape>(sender as DependencyObject);
                if (landscape != null)
                {
                    landscape.ZoomOnto(zoomTarget, false, true);
                    zoomTarget.RaiseEvent(new RoutedEventArgs(ZInformationLandscape.ClickZoomEvent, zoomTarget));
                }
            }
            e.Handled = true;
        }

        public static void OnTouchDown(Object sender, TouchEventArgs e)
        {
            if (InteractiveSurface.PrimarySurfaceDevice.IsFingerRecognitionSupported &&
                !e.TouchDevice.GetIsFingerRecognized())
            {
                e.Handled = true;
            }
        }

        #endregion

        #region ManipulationEvents

        public static void ManipulationInertiaStarting(object sender, ManipulationInertiaStartingEventArgs e)
        {
            var component = e.OriginalSource as ZComponent;

            if (component == null)
            {
                return;
            }

            e.Handled = true;

            ZObjectManipulationInformation info = ElementsBeingManipulated[component];

            Vector initialVelocity = e.InitialVelocities.LinearVelocity;

            if ((info.BounceBorder & Boundary.Top) == Boundary.Top || (info.BounceBorder & Boundary.Bottom) == Boundary.Bottom)
            {
                initialVelocity.Y = -initialVelocity.Y;
                e.TranslationBehavior.DesiredDisplacement = 200;
            }
            if ((info.BounceBorder & Boundary.Left) == Boundary.Left || (info.BounceBorder & Boundary.Right) == Boundary.Right)
            {
                initialVelocity.X = -initialVelocity.X;
                e.TranslationBehavior.DesiredDisplacement = 200;
            }
            if (info.BounceBorder == Boundary.None)
            {
                e.TranslationBehavior.DesiredDeceleration = 0.002f;
            }

            e.TranslationBehavior.InitialVelocity = initialVelocity;
            info.BounceBorder = Boundary.None;
        }

        public static void ManipulationCompleted(object sender, ManipulationCompletedEventArgs e)
        {
            var component = e.OriginalSource as ZComponent;

            if (component == null)
            {
                return;
            }

            component.IsBeingManipulated = false;
            if (ElementsBeingManipulated.ContainsKey(component))
            {
                ZObjectManipulationInformation info = ElementsBeingManipulated[component];
                //Raise DragFinishedEvent and set IsDragging to false
                var args = new ZObjectDragDropEventArgs();
                args.RoutedEvent = ZObjectDragDropBehavior.DragFinishedEvent;
                args.Source = info.ComponentBeingManipulated;

                //TODO: e.OriginX/e.OriginY == e.ManipulationOrigin.X/Y ?
                args.CurrentDragPoint = new Point(e.ManipulationOrigin.X, e.ManipulationOrigin.Y);

                info.ComponentBeingManipulated.RaiseEvent(args);

                ElementsBeingManipulated.Remove(component);
            }
        }

        public static void ManipulationDelta(object sender, ManipulationDeltaEventArgs e)
        {
            var component = e.OriginalSource as ZComponent;

            if (component == null)
            {
                return;
            }

            e.Handled = true;

            // TODO: dragging works well with following code, without own dragging method --> 
            // evaluate flag for dragging true/false somewhere below
            ZObjectManipulationInformation info = ElementsBeingManipulated[component];

            if (info.LastManipulationOrigin == e.ManipulationOrigin && e.DeltaManipulation.Translation.X == 0.0 &&
                e.DeltaManipulation.Translation.Y == 0.0 && e.DeltaManipulation.Rotation == 0.0 && e.DeltaManipulation.Expansion.X == 0.0)
            {
                return;
            }

            if (ZObjectResizeBehavior.GetIsResizable(component) || ZObjectDragDropBehavior.GetIsDraggable(component))
            {
                #region Fire Drag Events, TODO: extract this to extra method

                //if (ZObjectBlubAndBinDragDropBehavior.GetIsDraggable(comp) && e.ScaleDelta == 1 && e.CumulativeTranslation.Length > 10)
                if (ZObjectDragDropBehavior.GetIsDraggable(component) && e.DeltaManipulation.Scale.X == 1 && e.DeltaManipulation.Scale.Y == 1 &&
                    e.CumulativeManipulation.Translation.X > 10 && e.CumulativeManipulation.Translation.Y > 10)
                {
                    //if we are not dragging yet fire drag started event and set is dragging to true
                    if (!ZObjectDragDropBehavior.GetIsDragging(component))
                    {
                        var args = new ZObjectDragDropEventArgs
                            {
                                RoutedEvent = ZObjectDragDropBehavior.DragStartedEvent,
                                Source = component,
                                CurrentDragPoint = new Point(e.ManipulationOrigin.X, e.ManipulationOrigin.Y)
                            };
                        component.RaiseEvent(args);
                        ZObjectDragDropBehavior.SetIsDragging(component, true);
                    }
                        //otherwise fire only drag delta event
                    else
                    {
                        if (info.DeltaFired)
                        {
                            info.DeltaFired = false;
                        }
                        else
                        {
                            info.DeltaFired = true;
                            //Raise DragDeltaEvent
                            var args = new ZObjectDragDropEventArgs();
                            args.CurrentDragPoint = new Point(e.ManipulationOrigin.X, e.ManipulationOrigin.Y);
                            args.RoutedEvent = ZObjectDragDropBehavior.DragDeltaEvent;
                            args.Source = component;
                            component.RaiseEvent(args);
                        }
                    }
                }
                else
                {
                    ZObjectDragDropBehavior.SetIsDragging(component, false);
                }

                #endregion
            }

            info.LastManipulationOrigin = e.ManipulationOrigin;
            Point point = e.ManipulationOrigin - e.DeltaManipulation.Translation;
            Vector center = info.Center - point;

            bool canMove = ZObjectDragDropBehavior.GetIsDraggable(component);
            bool canRotate = ZObjectRotateBehavior.GetIsRotatable(component);
            bool canResize = ZObjectResizeBehavior.GetIsResizable(component);

            double angle = canRotate ? NormalizeAngle(e.DeltaManipulation.Rotation) : 0.0;
            double scale = canResize ? e.DeltaManipulation.Scale.X : 1.0;
            Vector translation = canMove ? e.DeltaManipulation.Translation : new Vector(0, 0);

            bool ignoreXTranslation = false;
            bool ignoreYTranslation = false;
            bool ignoreRotation = false;

            CheckTouchesForOutOfBoundsTracking(component, ref ignoreXTranslation, ref ignoreYTranslation, ref ignoreRotation);

            if (canMove && angle != 0)
            {
                Vector v2 = RotateVector(center, angle);
                translation += v2 - center;
            }

            if (canMove && scale != 1)
            {
                Vector v3 = center*scale;
                translation += v3 - center;
            }

            translation.X = ignoreXTranslation ? 0.0 : translation.X;
            translation.Y = ignoreYTranslation ? 0.0 : translation.Y;

            Boundary boundary = info.ParentLandscapes.First().EnableBoundaries
                                    ? ConstrainDelta(component, ref translation, ref angle, ref scale, info.Center,
                                                     new Rect(component.ParentInformationLandscape.TransformationBorder.RenderSize))
                                    : Boundary.None;
            if (boundary != Boundary.None && e.IsInertial)
            {
                info.BounceBorder = boundary;
                e.StartInertia();
                return;
            }

            if (angle != 0)
            {
                component.Angle += angle;
            }

            if (scale != 1)
            {
                component.Width *= scale;
                component.Height *= scale;
            }

            if (translation.LengthSquared > 0)
            {
                var centerPoint = new Point(info.Center.X + translation.X, info.Center.Y + translation.Y);
                Vector t = GetOffset(centerPoint, angle, component.ActualWidth, component.ActualHeight, component.RenderTransformOrigin);

                Canvas.SetLeft(component, t.X);
                Canvas.SetTop(component, t.Y);

                info.Center = centerPoint;
            }
        }

        public static void ManipulationStarting(object sender, ManipulationStartingEventArgs e)
        {
            var comp = e.OriginalSource as ZComponent;

            if (comp == null)
            {
                return;
            }

            var info = new ZObjectManipulationInformation(comp);
            info.ElementBeingDragged = ZBehavior.FindCanvasChild(sender as UIElement);
            //info.OriginalPositionOnWindow = e.Device.GetPosition(null);

            info.OriginalFrameworkElement = (e.OriginalSource as FrameworkElement ?? info.ElementBeingDragged as FrameworkElement);

            // TODO: replace using TransformToAncestor()
            info.ParentLandscapes = ZTreeHelper.TryFindParents<ZInformationLandscape>(comp);
            info.Left = Canvas.GetLeft(comp);
            info.Top = Canvas.GetTop(comp);

            info.Touches.Add(e.Device as TouchDevice);

            info.OriginalPositionOnCanvas = new Point(info.Left, info.Top);

            info.LastManipulationOrigin = new Point();

            Point center = comp.TranslatePoint(new Point(comp.Width/2, comp.Height/2), comp.ParentInformationLandscape.TransformationBorder);
            info.Center = center;

            ElementsBeingManipulated.Add(comp, info);

            e.ManipulationContainer = comp.ParentInformationLandscape.TransformationBorder;
            comp.IsBeingManipulated = true;
            e.Handled = true;
        }

        #endregion

        #region Utils

        [Flags]
        public enum Boundary
        {
            None,
            Left,
            Top,
            Right,
            Bottom
        }

        private static Rectangle debugRect;

        private static Rect GetItemBounds(Size componentSize, Point renderTransformOrigin, Vector translation, double angle, double scale,
                                          Point center)
        {
            var renderOrigin = new Point(componentSize.Width*renderTransformOrigin.X*scale, componentSize.Height*renderTransformOrigin.Y*scale);
            Vector offset = GetOffset(center + translation, angle, componentSize.Width*scale, componentSize.Height*scale, renderTransformOrigin);

            Matrix renderMatrix = GetRenderMatrix(renderOrigin, offset, angle);

            Rect itemBounds =
                new MatrixTransform(renderMatrix).TransformBounds(new Rect(new Size(componentSize.Width*scale, componentSize.Height*scale)));
            return itemBounds;
        }

        private static Boundary ConstrainDelta(ZComponent component, ref Vector translation, ref double angle, ref double scale, Point center,
                                               Rect containerBounds)
        {
            if (translation.LengthSquared == 0 && scale == 1 && angle == 0)
            {
                return Boundary.None;
            }

            var boundary = Boundary.None;
            Rect itemBounds = GetItemBounds(component.RenderSize, component.RenderTransformOrigin, translation, component.Angle + angle, scale, center);

            if (!containerBounds.Contains(itemBounds))
            {
                var edges = new[] {itemBounds.TopLeft, itemBounds.TopRight, itemBounds.BottomRight, itemBounds.BottomLeft};
                foreach (Point edge in edges)
                {
                    if (!containerBounds.Contains(edge))
                    {
                        if (edge.X < 0)
                        {
                            boundary |= Boundary.Left;
                        }
                        else if (edge.X > containerBounds.Right)
                        {
                            boundary |= Boundary.Right;
                        }
                        if (edge.Y < 0)
                        {
                            boundary |= Boundary.Top;
                        }
                        else if (edge.Y > containerBounds.Bottom)
                        {
                            boundary |= Boundary.Bottom;
                        }
                    }
                }
                Vector vector = default(Vector);
                double num = 0.0;
                int num2 = 0;
                if (translation.LengthSquared > 0.0)
                {
                    num2 = (int) translation.Length + 1;
                }
                else
                {
                    if (angle != 0.0)
                    {
                        num2 = (int) (2.0*Math.Abs(angle)) + 1;
                    }
                }
                if (num2 > 0)
                {
                    var vector2 = new Vector(translation.X/num2, translation.Y/num2);
                    double num3 = angle/num2;
                    for (int i = 0; i < num2; i++)
                    {
                        if (
                            containerBounds.Contains(GetItemBounds(component.RenderSize, component.RenderTransformOrigin, vector + vector2,
                                                                   component.Angle + num + num3, 1, center)))
                        {
                            vector += vector2;
                            num += num3;
                        }
                        else
                        {
                            i--;
                            if (vector2.X != 0.0)
                            {
                                vector2.X = 0.0;
                            }
                            else
                            {
                                if (vector2.Y == 0)
                                {
                                    break;
                                }
                                vector2.Y = 0.0;
                                if (translation.X != 0)
                                {
                                    vector2.X = translation.X/num2;
                                }
                            }
                        }
                    }
                }
                scale = 1;
                angle = num;
                translation = vector;
            }

            //Debug
            /*
            Rect newItemBounds = GetItemBounds(component.RenderSize, component.RenderTransformOrigin, translation, component.Angle + angle, scale, center);

            if (debugRect == null)
            {
                Rectangle rect = new Rectangle()
                {
                    Stroke = new SolidColorBrush(Colors.Yellow),
                    StrokeThickness = 1
                };
                ((Canvas)component.ParentInformationLandscape.Content).Children.Add(rect);
                debugRect = rect;
            }
            debugRect.Width = newItemBounds.Width;
            debugRect.Height = newItemBounds.Height;
            Canvas.SetLeft(debugRect, newItemBounds.X);
            Canvas.SetTop(debugRect, newItemBounds.Y);*/

            return boundary;
        }

        internal static Vector GetOffset(Point center, double angle, double width, double height, Point renderTransformOrigin)
        {
            var renderOrigin = new Point(width*renderTransformOrigin.X, height*renderTransformOrigin.Y);
            Point point = GetRenderMatrix(renderOrigin, new Vector(0.0, 0.0), angle).Transform(new Point(width*0.5, height*0.5));
            return center - point;
        }

        private static Matrix GetRenderMatrix(Point renderOrigin, Vector offset, double rotation)
        {
            Matrix identity = Matrix.Identity;
            identity.RotateAt(rotation, renderOrigin.X, renderOrigin.Y);
            identity.Translate(offset.X, offset.Y);
            return identity;
        }

        private static void CheckTouchesForOutOfBoundsTracking(ZComponent component, ref bool ignoreXTranslation, ref bool ignoreYTranslation,
                                                               ref bool ignoreRotation)
        {
            ZInformationLandscape landscape = component.ParentInformationLandscape;
            int num = 0;
            foreach (InputDevice current in component.GetInputDevicesCaptured())
            {
                num++;
                Point position = current.GetPosition(landscape);
                var componentRect = new Rect(new Point(Canvas.GetLeft(component), Canvas.GetTop(component)), component.RenderSize);
                if (componentRect.Contains(position))
                {
                    ignoreXTranslation = false;
                    ignoreYTranslation = false;
                    return;
                }
                if (landscape != null)
                {
                    ignoreXTranslation = (ignoreXTranslation || position.X < 0.0 || position.X > landscape.ActualWidth);
                    ignoreYTranslation = (ignoreYTranslation || position.Y < 0.0 || position.Y > landscape.ActualHeight);
                }
            }
            ignoreRotation = ((ignoreXTranslation ^ ignoreYTranslation) && num == 1);
        }

        private static Vector RotateVector(Vector vector, double degrees)
        {
            double num = degrees*3.14159265358979/180;
            double num1 = Math.Sin(num);
            double num2 = Math.Cos(num);
            double x = vector.X*num2 - vector.Y*num1;
            double x1 = vector.X*num1 + vector.Y*num2;
            return new Vector(x, x1);
        }

        private static double NormalizeAngle(double angle)
        {
            double num = angle%360;
            if (num <= 180)
            {
                if (num <= -180)
                {
                    num = num + 360;
                }
            }
            else
            {
                num = num - 360;
            }
            return num;
        }

        #endregion
    }
}